# coding=utf-8

import asyncio
import os
import sys
import time
import logging
import logging.handlers
from datetime import datetime
from logging import (
    getLogger, INFO, WARN, DEBUG, ERROR,
    CRITICAL, getLevelName)
from typing import Callable, Tuple, TypeVar, Any, cast


LOG_FILE_MAX_BYTES = 31457280
LOG_FILE_BACKUP_COUNT = 1000
LOG_LEVEL = logging.DEBUG

FORMAT = '''%(message)s'''
formatter = logging.Formatter(FORMAT)


level_dict = {
    "debug": DEBUG,
    "DEBUG": DEBUG,
    "info": INFO,
    "INFO": INFO,
    "warn": WARN,
    "WARN": WARN,
    "error": ERROR,
    "ERROR": ERROR
}


def format_msg(level: int, msg: str) -> str:
    back_file = sys._getframe().f_back.f_back.f_back  # noqa
    rel_name = back_file.f_globals["__name__"].replace(".", "/") + ".py"
    now = datetime.now()
    cur_time = now.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "%+03d00" % (-time.timezone // 3600)
    return f"{cur_time}|{getLevelName(level)}|{rel_name}:{back_file.f_lineno}|{back_file.f_code.co_name}|{msg}"


def _handle_mutable_args(msg: str, *args, **kwargs) -> str:
    for arg in args:
        msg = f"{msg}|{arg}"
    for k, v in kwargs.items():
        msg = f"{msg}|{k}={v}"
    return msg


class EspoirLog(object):

    global_module_name: str = None
    global_log_level: int = LOG_LEVEL
    global_log_dir: str = None
    global_log_type: str = "both"    # log_type: both|log|print

    def __init__(self):
        super().__init__()
        self._normal = None
        self._error = None
        # 存放上一次打印日志的时间(字符串)
        self.last_date = time.strftime('%Y-%m-%d', time.localtime(time.time()))
        self.default_keys = {}

    @property
    def level(self) -> int:
        return self.global_log_level

    @property
    def name(self) -> str:
        return self.global_module_name

    @property
    def log_dir(self) -> str:
        return self.global_log_dir

    @property
    def log_type(self) -> str:
        return self.global_log_type

    @classmethod
    def init_log_config(cls, module_name, log_dir=None, level="debug",
                        log_type="both"):
        cls.global_module_name = module_name
        cls.global_log_type = log_type
        if "print" != cls.global_log_type:
            if log_dir:
                cls.set_log_dir(log_dir)
            cls.global_log_level = level_dict.get(level)

    @classmethod
    def set_log_dir(cls, log_dir):
        """
        设置日志目录
        :param log_dir: string, absolute path, like "/data/logs/"
        :return:
        """
        cls.global_log_dir = log_dir
        if not os.path.exists(log_dir):
            os.system("mkdir -p %s" % log_dir)

    def get_normal_log(self, date_format):
        file_name = f'{self.log_dir}/{self.name}_{date_format}_all.log'
        normal_handler = logging.handlers.RotatingFileHandler(
            file_name, maxBytes=LOG_FILE_MAX_BYTES,
            backupCount=LOG_FILE_BACKUP_COUNT)
        normal_handler.setFormatter(formatter)
        normal_log = getLogger(self.name)
        normal_log.setLevel(self.level)
        normal_log.addHandler(normal_handler)
        return normal_log

    def get_error_log(self, date_format):
        file_name = f'{self.log_dir}/err_{self.name}_{date_format}.log'
        error_handler = logging.handlers.RotatingFileHandler(
            file_name, maxBytes=LOG_FILE_MAX_BYTES,
            backupCount=LOG_FILE_BACKUP_COUNT)
        error_handler.setFormatter(formatter)
        error_log = getLogger(self.name + '_error')
        error_log.setLevel(LOG_LEVEL)
        error_log.addHandler(error_handler)
        return error_log

    @property
    def normal_log(self):
        cur_date = time.strftime('%Y-%m-%d', time.localtime(time.time()))
        if not self._normal or self.last_date != cur_date:
            self.last_date = cur_date
            if self.name in logging.Logger.manager.loggerDict:
                logging.Logger.manager.loggerDict.pop(self.name)
            self._normal = self.get_normal_log(self.last_date)
        return self._normal

    @property
    def error_log(self):
        cur_date = time.strftime('%Y-%m-%d', time.localtime(time.time()))
        if not self._error or self.last_date != cur_date:
            self.last_date = cur_date
            name = self.name + '_error'
            if name in logging.Logger.manager.loggerDict:
                logging.Logger.manager.loggerDict.pop(name)
            self._error = self.get_error_log(self.last_date)
        return self._error

    def add_default_keys(self, key: str, value: Any):
        self.default_keys[key] = value

    def remove_default_keys(self, key: str):
        if key in self.default_keys:
            self.default_keys.pop(key)

    def sync_default_keys(self):
        return

    def _handle_default_keys(self, msg):
        self.sync_default_keys()
        if self.default_keys:
            for k, v in self.default_keys.items():
                msg = f"{msg}|{k}={v}"
        return msg

    def _handle_log(self, level: int, tip: str, *args, **kwargs):
        if self.level <= level:
            msg = self._handle_default_keys(tip)
            msg = _handle_mutable_args(msg, *args, **kwargs)
            msg = format_msg(level, msg)

            if self.log_type != "log":
                print(msg)

            if self.log_type != "print":
                self.normal_log._log(level, msg, [])  # noqa

    def debug(self, tip: str, *args, **kwargs):
        self._handle_log(DEBUG, tip, *args, **kwargs)

    def info(self, tip: str, *args, **kwargs):
        self._handle_log(INFO, tip, *args, **kwargs)

    def warning(self, tip: str, *args, **kwargs):
        self._handle_log(WARN, tip, *args, **kwargs)

    def warn(self, tip: str, *args, **kwargs):
        self._handle_log(WARN, tip, *args, **kwargs)

    def error(self, tip: str, *args, **kwargs):
        self._handle_log(ERROR, tip, *args, **kwargs)

    def critical(self, tip: str, *args, **kwargs):
        self._handle_log(CRITICAL, tip, *args, **kwargs)


global_log = EspoirLog()


T = TypeVar('T', bound=Callable[..., Any])


def pre_wrapper(*args: Tuple[Any], **kwargs: Any) -> str:
    input_args = ', '.join(
        [str(arg) for arg in args] +
        [f"{k}={v}" for k, v in kwargs.items()]
    )
    input_args = (
            input_args[: 252] + '...') if len(input_args) > 255 else input_args
    return input_args


def log_record(func: T) -> T:
    async def async_wrapper(*args: Tuple[Any], **kwargs: Any) -> Any:
        self = args[0] if isinstance(args[0], EspoirLog) else None
        log_func = self.info if self else global_log.info     # noqa

        input_args = pre_wrapper(*args, **kwargs)
        log_func(f"{func.__name__}|start|{input_args}")

        result = await func(*args, **kwargs)

        output_result = str(result)
        output_result = (
            output_result[:252] + '...') if len(output_result) > 255 else output_result
        log_func(f"[{func.__name__}][done][{output_result}]")

        return result

    def wrapper(*args: Tuple[Any], **kwargs: Any) -> Any:
        self = args[0] if isinstance(args[0], EspoirLog) else None
        log_func = self.info if self else global_log.info      # noqa

        input_args = pre_wrapper(*args, **kwargs)
        log_func(f"{func.__name__}|start|{input_args}")

        result = func(*args, **kwargs)

        output_result = str(result)
        output_result = (
            output_result[:252] + '...') if len(output_result) > 255 else output_result
        log_func(f"{func.__name__}|done|{output_result}")

        return result

    if asyncio.iscoroutinefunction(func):
        return cast(T, async_wrapper)
    else:
        return cast(T, wrapper)
